/*
 * Copyright:  2018 smarabbit studio.
 *
 *  Licensed under the Confluent Community License; you may not use this file
 *  except in compliance with the License.  You may obtain a copy of the License at
 *
 *  http://www.confluent.io/confluent-community-license
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations under the License.
 *
 *  @作   者： 黄开晖 (117227773@qq.com)
 *  @日   期:  2021年6月12日
 */
package com.massyframework.beanskin.graphql.servlet;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.massyframework.beanskin.Module;
import com.massyframework.beanskin.ModuleRuntimeException;
import com.massyframework.beanskin.framework.FrameworkEventAdapter;
import com.massyframework.beanskin.framework.FrameworkStartedEvent;
import com.massyframework.beanskin.framework.FrameworkStoppingEvent;
import com.massyframework.beanskin.graphql.context.GraphQLModular;
import com.massyframework.beanskin.graphql.fetching.AutoExtendDirective;
import com.massyframework.beanskin.graphql.subscription.SubscriptionExecutionStrategyEx;
import com.massyframework.beanskin.service.ServiceEvent;
import com.massyframework.beanskin.service.ServiceListener;
import com.massyframework.beanskin.service.ServiceRegistedEvent;
import com.massyframework.beanskin.service.ServiceRepository;
import com.massyframework.beanskin.service.ServiceUnregistingEvent;
import com.massyframework.beanskin.util.IOUtils;
import com.massyframework.beanskin.util.exceptioncollect.ExceptionCollectUtils;
import com.massyframework.beanskin.util.sorting.graph.GraphFactory;
import com.massyframework.beanskin.util.sorting.graph.SortResult;

import graphql.execution.preparsed.NoOpPreparsedDocumentProvider;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.kickstart.execution.GraphQLObjectMapper;
import graphql.kickstart.execution.GraphQLQueryInvoker;
import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider;
import graphql.kickstart.execution.config.ExecutionStrategyProvider;
import graphql.kickstart.execution.config.InstrumentationProvider;
import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler;
import graphql.kickstart.execution.error.GraphQLErrorHandler;
import graphql.kickstart.execution.instrumentation.NoOpInstrumentationProvider;
import graphql.kickstart.servlet.GraphQLConfiguration;
import graphql.kickstart.servlet.apollo.ApolloScalars;
import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider;
import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider;
import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder;
import graphql.kickstart.servlet.context.GraphQLServletContextBuilder;
import graphql.kickstart.servlet.core.DefaultGraphQLRootObjectBuilder;
import graphql.kickstart.servlet.core.GraphQLServletListener;
import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder;
import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory;
import graphql.schema.DataFetcher;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import graphql.schema.TypeResolver;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaDirectiveWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaGeneratorPostProcessing;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

/**
 * GraphQLModular模式构建器
 * @author huangkh
 *
 */
public class GraphQLModularSchemalBuilder extends FrameworkEventAdapter implements ServiceListener {

	private Logger logger = LoggerFactory.getLogger(GraphQLModularSchemalBuilder.class);
	
	private final List<GraphQLServletListener> listeners = new ArrayList<>();
	private GraphQLErrorHandler errorHandler = new DefaultGraphQLErrorHandler();
	
	private GraphQLSchemaServletProvider schemaProvider;
	private GraphQLServletContextBuilder contextBuilder = new DefaultGraphQLServletContextBuilder();
	private GraphQLServletRootObjectBuilder rootObjectBuilder = new DefaultGraphQLRootObjectBuilder();
	private PreparsedDocumentProvider preparsedDocumentProvider =
		      NoOpPreparsedDocumentProvider.INSTANCE;
	private InstrumentationProvider instrumentationProvider = new NoOpInstrumentationProvider();
	
	private AsyncExecutionStrategyEx executionStragegy = new AsyncExecutionStrategyEx();
	private ExecutionStrategyProvider executionStrategyProvider =
		      new DefaultExecutionStrategyProvider(
		    		  executionStragegy, executionStragegy, new SubscriptionExecutionStrategyEx());
	
	private Module<?> module;
	private ServiceRepository serviceRepo;
	private URL commonSchemaFile;
	private Set<GraphQLModular> modulars;
	
	private boolean started = false;

	/**
	 * 
	 */
	public GraphQLModularSchemalBuilder(Module<?> module, URL url) {
		this.module = Objects.requireNonNull(module, "\"module\" cannot be null.");
		this.serviceRepo = ServiceRepository.retrieveFrom(this.module);
		this.commonSchemaFile = Objects.requireNonNull(url, "\"url\" cannot be null.");
		
		this.modulars = new CopyOnWriteArraySet<>();
	}
		
	@Override
	public synchronized void onEvent(FrameworkStartedEvent event) {
		super.onEvent(event);
		if (!this.started) {
			this.started = true;
			this.serviceRepo.addListener(this);
			
			List<GraphQLModular> modulars = this.serviceRepo.getServices(GraphQLModular.class);
			this.modulars.addAll(modulars);
					
			this.updateSchema();
		}
	}

	@Override
	protected synchronized void onEvent(FrameworkStoppingEvent event) {
		if (this.started) {
			this.started = false;
			this.serviceRepo.removeListener(this);
			this.modulars.clear();
			this.updateSchema();
			super.onEvent(event);
		}
	}

	@Override
	public final void onChanged(ServiceEvent<?> event) {
		Class<?>[] types = event.getServiceReference().getExportTypes();
		for (Class<?> type: types) {
			if (type == GraphQLModular.class) {
				if (event instanceof ServiceRegistedEvent) {
					GraphQLModular modular = 
						(GraphQLModular)this.serviceRepo.getService(event.getServiceReference()).orElse(null);
					if (modular != null) {
						if (this.modulars.add(modular)) {
							this.updateSchema();
						}
					}
				}else {
					if (event instanceof ServiceUnregistingEvent) {
						GraphQLModular modular = 
								(GraphQLModular)this.serviceRepo.getService(event.getServiceReference()).orElse(null);
						if (modular != null) {
							if (this.modulars.remove(modular)) {
								this.updateSchema();
							}
						}
					}
				}
			}
		}
	}
		
	GraphQLSchemaServletProvider getSchemaProvider() {
	    return schemaProvider;
	}
	
	/**
	 * 更新GraphQL模型
	 */
	void updateSchema() {
		GraphQLSchema schema = this.doUpdateSchema();
		if (schema != null) {
			this.schemaProvider =  new DefaultGraphQLSchemaServletProvider(schema);
		}
	}
	
	/**
	 * 更新GraphQLSchema
	 */
	GraphQLSchema doUpdateSchema() {
		try {
			List<GraphQLModular> list = this.sort();
			
			SchemaParser schemaParser = new SchemaParser();
			SchemaGenerator schemaGenerator = new SchemaGenerator();
			
			TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();
			this.loadModel(schemaParser, this.commonSchemaFile, typeRegistry);
			
			RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
			//占位方法
			builder.type("Query", typeWire -> typeWire.dataFetcher("placeholderFind", env -> ""));
			builder.type("Mutation", tyepWire -> tyepWire.dataFetcher("placeholderUpdate", env -> ""));
			builder.type("Subscription", typeWire-> typeWire.dataFetcher("placeholderEvent", env -> null));

			builder.scalar(ApolloScalars.Upload);
			builder.directiveWiring(new AutoExtendDirective());
			
			if (!list.isEmpty()) {				
				for (GraphQLModular modular: list) {
					TypeDefinitionRegistry registry =
							modular.getTypeDefinitionRegistry(schemaParser);
					if (registry != null) {
						typeRegistry.merge(registry);
						RuntimeWiring wiring = modular.getRuntimeWiring();
						if (wiring != null) {
							this.mergeRuntimeWiring(builder, wiring);
						}
					}
				}
			}
			
			
			return schemaGenerator.makeExecutableSchema(typeRegistry, builder.build());
		}catch(Exception e) {
			if (logger.isErrorEnabled()) {
				logger.error(e.getMessage(), e);
			}
			
			ExceptionCollectUtils.addException(e);
			return null;
		}
	}
	
	/**
	 * 合并RuntimeWiring
	 * @param builder {@link RuntimeWiring.Builder}
	 * @param wiring {@link RuntimeWiring}
	 */
	@SuppressWarnings("rawtypes")
	void mergeRuntimeWiring(RuntimeWiring.Builder builder, RuntimeWiring wiring) {
		if (wiring == null) return;
		
		//DataFetcher
		Map<String, Map<String, DataFetcher>> dataFetchersMap = wiring.getDataFetchers();
		for (Map.Entry<String, Map<String, DataFetcher>> entry: dataFetchersMap.entrySet()) {
						
			String name = entry.getKey();
			Map<String, DataFetcher> map = entry.getValue();
			for (Map.Entry<String, DataFetcher> sub: map.entrySet()) {
				String fieldName = sub.getKey();
				DataFetcher dataFetcher = sub.getValue();
				
				builder.type(
					name,
					typeWiring -> typeWiring
						.dataFetcher(fieldName, dataFetcher)
					);
			}
		}
		
		//scalars
		Map<String, GraphQLScalarType> scalarMap = wiring.getScalars();
		for (Map.Entry<String, GraphQLScalarType> entry: scalarMap.entrySet()) {
			builder.scalar(entry.getValue());
		}
		
		//typeResolvers
		Map<String, TypeResolver> typeResolverMap = wiring.getTypeResolvers();
		for (Map.Entry<String, TypeResolver> entry: typeResolverMap.entrySet()) {
			builder.type(
					entry.getKey(),
					typeWiring -> typeWiring.typeResolver(entry.getValue()));
		}
		
		//directiveWiring
		List<SchemaDirectiveWiring> directiveWirings = wiring.getDirectiveWiring();
		for (SchemaDirectiveWiring direciveWiring: directiveWirings) {
			builder.directiveWiring(direciveWiring);
		}
				
		//SchemaGeneratorPostProcessing
		Collection<SchemaGeneratorPostProcessing> processings = wiring.getSchemaGeneratorPostProcessings();
		for (SchemaGeneratorPostProcessing processing: processings) {
			builder.transformer(processing);
		}
	}
	
	/**
	 * 加载公共的Graphql模型文件
	 * @param schemaParser {@link SchemaParser}
	 * @param url {@link URL},模型文件url
	 * @return {@link TypeDefinitionRegistry}
	 */
	void loadModel(SchemaParser schemaParser, URL url, TypeDefinitionRegistry registry){	
		InputStream is = null;
		InputStreamReader reader = null;
		try {
			is = url.openStream();
			reader = new InputStreamReader(is);
			registry.merge(schemaParser.parse(reader));	
		}catch(Exception e) {
			if (logger.isErrorEnabled()) {
				logger.error("load graphql file failed.", e);
			}
			ExceptionCollectUtils.addException(new ModuleRuntimeException(this.module, e));
		}finally {
			IOUtils.close(reader);
			IOUtils.close(is);
		}
	}
	
	/**
	 * 对GraphQL模块进行排序
	 * @return {@link List}
	 */
	List<GraphQLModular> sort(){
		SortResult<GraphQLModular> sortResult = GraphFactory.sort(new ArrayList<>(this.modulars));
		return sortResult.getSorted();
	}
	
	/**
	 * 构建GraphQL配置
	 * @return {@link GraphQLConfiguration}
	 */
	public GraphQLConfiguration buildConfiguration() {
		return GraphQLConfiguration.with(buildInvocationInputFactory())
				.with(buildQueryInvoker())
				
				.with(buildObjectMapper()).with(listeners).build();
	}

	private GraphQLInvocationInputFactory buildInvocationInputFactory() {
		return GraphQLInvocationInputFactory.newBuilder(this::getSchemaProvider)
				.withGraphQLContextBuilder(contextBuilder).withGraphQLRootObjectBuilder(rootObjectBuilder).build();
	}

	private GraphQLQueryInvoker buildQueryInvoker() {
		return GraphQLQueryInvoker.newBuilder().withPreparsedDocumentProvider(preparsedDocumentProvider)
				.withInstrumentation(() -> instrumentationProvider.getInstrumentation())
				.withExecutionStrategyProvider(executionStrategyProvider)
				
				.build();
	}

	private GraphQLObjectMapper buildObjectMapper() {
		return GraphQLObjectMapper.newBuilder().withGraphQLErrorHandler(errorHandler).build();
	}
}
